package org.pageley.games.sanctuary.domain.entity.creature;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.UUID;

import org.pageley.games.sanctuary.domain.Dice;
import org.pageley.games.sanctuary.domain.Direction;
import org.pageley.games.sanctuary.domain.Location;
import org.pageley.games.sanctuary.domain.environment.Environment;
import org.pageley.games.sanctuary.domain.environment.pathfinding.AStarAlgorithm;
import org.pageley.games.sanctuary.domain.environment.pathfinding.ManhattanHeuristic;
import org.pageley.games.sanctuary.domain.environment.pathfinding.PathFindingAlgorithm;
import org.pageley.games.sanctuary.domain.event.ActionPointsEvent;
import org.pageley.games.sanctuary.domain.event.Event;
import org.pageley.games.sanctuary.domain.event.EventListener;
import org.pageley.games.sanctuary.domain.event.HitEvent;
import org.pageley.games.sanctuary.domain.event.InsufficientActionPointsEvent;
import org.pageley.games.sanctuary.domain.event.KilledEvent;
import org.pageley.games.sanctuary.domain.event.MissEvent;
import org.pageley.games.sanctuary.domain.event.MoveEvent;

public class SimpleCreature implements Creature {

	private Location location;
	private Environment environment;
	private Collection<EventListener> eventListeners;
	private UUID id;
	private PathFindingAlgorithm pathFindingAlgorithm;
	private CreatureProfile profile;
	private int actionPointsRemaining;
	private int hitPointsRemaining;
	
	public SimpleCreature(Location location, CreatureProfile profile) {
		this.location = location;
		this.profile = profile;
		this.eventListeners = new HashSet<EventListener>();
		this.id = UUID.randomUUID();
		this.pathFindingAlgorithm = new AStarAlgorithm(new ManhattanHeuristic());
		this.hitPointsRemaining = profile.getHitPoints();
		endTurn();
	}

	@Override
	public void move(Direction direction) {
		moveTo(this.location.locationIn(direction));
	}

	private void notifyListeners(Event event) {
		for(EventListener listener : eventListeners) {
			listener.eventOccured(event);
		}
	}

	@Override
	public Location getLocation() {
		return this.location;
	}

	@Override
	public void setEnvironment(Environment environment) {
		this.environment = environment;
	}

	@Override
	public void addEventListener(EventListener eventListener) {
		this.eventListeners.add(eventListener);
	}

	@Override
	public UUID getId() {
		return id;
	}

	@Override
	public void moveTo(Location newLocation) {
		List<Location> path = Collections.emptyList();
		Location start = this.location;
		if(this.environment != null) {
			Collection<Location> nodes = this.environment.getNodes();
			nodes.add(start);
			path = this.pathFindingAlgorithm.findPath(start, newLocation, nodes);
			boolean insufficientActionPoints = false;
			if(path.size() > this.actionPointsRemaining) {
				path = path.subList(0, this.actionPointsRemaining);
				insufficientActionPoints = true;
			}
			if(!path.isEmpty()) {
				this.actionPointsRemaining -= path.size();
				this.location = path.get(path.size() - 1);
				createEvents(start, path);
				notifyListeners(new ActionPointsEvent(this, this.actionPointsRemaining));
				if(insufficientActionPoints) {
					notifyListeners(new InsufficientActionPointsEvent(this));
				}
			}
		} else {
			this.location = newLocation;
		}
	}

	private void createEvents(Location start, List<Location> path) {
		Location previousLocation = start;
		Direction previousDirection = null;
		int distance = 0;
		for(Location location : path) {
			Direction direction = Direction.directionOf(previousLocation, location);
			if(direction == null) {
				throw new IllegalStateException("Unexpeced path gap.");
			}
			if(direction != previousDirection) {
				if(distance > 0) {
					notifyListeners(new MoveEvent(this, previousDirection, distance));
				}
				distance = 1;
				previousDirection = direction;
			} else {
				distance++;
			}
			previousLocation = location;
		}
		if(distance > 0) {
			notifyListeners(new MoveEvent(this, previousDirection, distance));
		}
	}

	@Override
	public int getActionPointsRemaining() {
		return actionPointsRemaining;
	}

	@Override
	public void endTurn() {
		this.actionPointsRemaining = profile.getActionPoints();
		notifyListeners(new ActionPointsEvent(this, this.actionPointsRemaining));
	}

	@Override
	public CreatureProfile getProfile() {
		return profile;
	}

	@Override
	public int getWeaponSkill() {
		return profile.getWeaponSkill();
	}

	@Override
	public double chanceToHit(Creature other) {
		int requiredRoll = requiredRollToHit(other);
		return Dice.ONE.chanceOfAtLeast(requiredRoll);
	}

	@Override
	public int attack(Creature other) {
		int damage = 0;
		if(isWithinRange(other) && hasEnoughActionPoints(3)) {
			int requiredRoll = requiredRollToHit(other);
			if(Dice.ONE.rollAtLeast(requiredRoll)) {
				damage = Dice.ONE.roll();
				other.hit(damage);
				notifyListeners(new HitEvent(this, other, damage));
				if(other.isDead()) {
					notifyListeners(new KilledEvent(other));
				}
			} else {
				notifyListeners(new MissEvent(this, other));
			}
			this.actionPointsRemaining -= 3;
			notifyListeners(new ActionPointsEvent(this, this.actionPointsRemaining));
		}
		return damage;
	}
	
	private boolean hasEnoughActionPoints(int actionPoints) {
		boolean result = true;
		if(this.actionPointsRemaining < actionPoints) {
			result = false;
			notifyListeners(new InsufficientActionPointsEvent(this));
		}
		return result;
	}
	
	private boolean isWithinRange(Creature other) {
		return this.location.squaresTo(other.getLocation()) <= 1;
	}

	private int requiredRollToHit(Creature other) {
		return 4 + (int) Math.floor((other.getWeaponSkill() - getWeaponSkill()) / 2);
	}

	@Override
	public int getHitPointsRemaining() {
		return hitPointsRemaining;
	}

	@Override
	public void hit(int damage) {
		if(damage < this.hitPointsRemaining) {
			this.hitPointsRemaining -= damage;
		} else {
			this.hitPointsRemaining = 0;
		}
	}

	@Override
	public boolean isDead() {
		return hitPointsRemaining == 0;
	}

}
